package de.stefanocke.roo.kontaktliste.web.common;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;

import org.apache.wicket.Component;
import org.apache.wicket.ajax.AjaxRequestTarget;
import org.apache.wicket.ajax.markup.html.form.AjaxFallbackButton;
import org.apache.wicket.markup.html.form.Button;
import org.apache.wicket.markup.html.form.Form;
import org.apache.wicket.markup.html.form.IFormModelUpdateListener;
import org.apache.wicket.markup.html.list.ListItem;
import org.apache.wicket.markup.html.panel.Panel;
import org.apache.wicket.markup.repeater.RepeatingView;
import org.apache.wicket.model.AbstractReadOnlyModel;
import org.apache.wicket.model.IModel;
import org.apache.wicket.model.IObjectClassAwareModel;
import org.apache.wicket.model.LoadableDetachableModel;
import org.apache.wicket.model.PropertyModel;
import org.h2.command.dml.Set;

import de.stefanocke.roo.kontaktliste.web.common.FormFieldsPanel.ButtonContainer;
import de.stefanocke.roo.wicket.RooStereotype;

/**
 * A form component panel that allows to edit a list of beans, including adding
 * or removing elements.
 * <p>
 * Loosely based on
 * http://wicketinaction.com/2008/10/building-a-listeditor-form-component/
 * 
 * @author stoc
 * 
 * @param <T> model type of the elements in the list
 */
@RooStereotype("wicket.ListEditor")
public abstract class ListEditorPanel<T> extends Panel implements IFormModelUpdateListener {

	/***/
	private static final long serialVersionUID = 1L;

	/**
	 * Represents the current state of the list that is edited. For example,
	 * this consists of some already existing entities and some freshly added.
	 * Since IModel is used here, the footprint is as small as possible. When
	 * using loadable / detachable entity models, only the IDs for the already
	 * existing entities are kept in the list.
	 */
	private List<IModel<? extends T>> list = new ArrayList<IModel<? extends T>>();

	private ListEditor listEditor;

	public ListEditorPanel(String id) {
		super(id);
		setOutputMarkupId(true);
		listEditor = new ListEditor("list");
		add(listEditor);
		Button addLink = new AjaxFallbackButton("add", null) {
			
			@Override
			protected void onSubmit(AjaxRequestTarget target, Form<?> form) {
				listEditor.addItem(createNewModel(getElementClass()));
				if(target!=null){
					//TODO: http://wicketinaction.com/2008/10/repainting-only-newly-created-repeater-items-via-ajax/
					target.add(getAjaxTargetComponent());
				}
				
			}
			
			@Override
			protected void onError(AjaxRequestTarget target, Form<?> form) {
				// TODO Auto-generated method stub
				
			}
		}.setDefaultFormProcessing(false);

		add(addLink);

	}
	
	public List<IModel<? extends T>> getList() {
		return Collections.unmodifiableList(list);
	}

	private class ListEditor extends RepeatingView {

		/***/
		private static final long serialVersionUID = 1L;

		public ListEditor(String id) {
			super(id);
		}

		@Override
		protected void onBeforeRender() {
			if (!hasBeenRendered()) {
				createItems();
			}
			super.onBeforeRender();
		}

		private void createItems() {
			for (int i = 0; i < list.size(); i++) {
				createItem(i);
			}
		}

		public void refreshItems() {
			this.removeAll();
			createItems();
		}

		private void createItem(final int index) {
			ListItem<IModel<? extends T>> li = new ListItem<IModel<? extends T>>(newChildId(), index,
					new AbstractReadOnlyModel<IModel<? extends T>>() {

						/***/
						private static final long serialVersionUID = 1L;

						@Override
						public IModel<? extends T> getObject() {
							return list.get(index);
						}
					});
			add(li);
			populateItem(li);
		}

		protected void populateItem(final ListItem<IModel<? extends T>> item) {
			Component itemComponent = createItemComponent("item", item.getModelObject());
			item.add(itemComponent);
			if (itemComponent instanceof ILabeled) {
				((ILabeled) itemComponent).setLabel(getItemLabelModel(item));
			}

			RepeatingView buttonsRepeater = ((ButtonContainer) itemComponent).getButtonsRepeater();
			
			Button removeLink = new IconButton(buttonsRepeater.newChildId(), null) {
				/***/
				private static final long serialVersionUID = 1L;


				@Override
				protected void onError(AjaxRequestTarget target, Form<?> form) {
					// TODO Auto-generated method stub
					
				}

				@Override
				protected void onSubmit(AjaxRequestTarget target, Form<?> form) {
					int idx = item.getIndex();

					for (int i = idx + 1; i < item.getParent().size(); i++) {
						ListItem<?> it = (ListItem<?>) item.getParent().get(i);
						it.setIndex(it.getIndex() - 1);
					}
					ListEditor.this.remove(item);
					list.remove(idx);
					if(target!=null){
						//TODO: http://wicketinaction.com/2008/10/repainting-only-newly-created-repeater-items-via-ajax/
						target.add(getAjaxTargetComponent());
					}
					
				}

				@Override
				protected String getKind() {
					return IconButton.DELETE;
				}

			}.setDefaultFormProcessing(false);
		
			buttonsRepeater.add(removeLink);
			

			

		}

		public void addItem(IModel<? extends T> value) {
			list.add(value);
			createItem(list.size() - 1);
		}

	};

	@Override
	protected void onInitialize() {
		super.onInitialize();
		// This is the first time, the CompoundPropertyModel of the parent is
		// available.
		onModelChanged();
	}

	@Override
	protected void onModelChanged() {
		
		IModel<?> model = getDefaultModel();
		Class<?> objectClass = getObjectClass(model);
		if(!Collection.class.isAssignableFrom(objectClass) && getOrderPropertyExpression()==null){
			throw new IllegalArgumentException("Expected IModel<? extends Collection>, but got IModel<"+objectClass+">. Path:"+getPath());
		}
		
		@SuppressWarnings("unchecked")
		IModel<Collection<T>> collectionModel = (IModel<Collection<T>>) model;
		Collection<T> modelObject = collectionModel.getObject();

		if (modelObject == null) {
			list = new ArrayList<IModel<? extends T>>();
		} else {
			list = new ArrayList<IModel<? extends T>>(createElementModelList(collectionModel));

		}
		listEditor.refreshItems();
	}

	private static Class<?> getObjectClass(IModel<?> model) {
		if(!(model instanceof IObjectClassAwareModel<?>)){
			throw new IllegalArgumentException("Expected IObjectClassAwareModel, but got "+model);
		}
		Class<?> objectClass = ((IObjectClassAwareModel<?>)model).getObjectClass();
		return objectClass;
	}
	
	protected String getOrderPropertyExpression(){
		return null;
	}

	protected Integer getOrderProperty(T element) {
		String orderProperty = getOrderPropertyExpression();
		if(orderProperty==null){
			throw new IllegalStateException("Expected OrderProperty, but is null. Path: "+getPath());
		}
		return (new PropertyModel<Integer>(element, orderProperty)).getObject();
	}
	
	protected void setOrderProperty(T element, Integer value) {
		String orderProperty = getOrderPropertyExpression();
		if(orderProperty!=null){
			(new PropertyModel<Integer>(element, orderProperty)).setObject(value);
		} 
	}
	

	@SuppressWarnings("unchecked")
	@Override
	public void updateModel() {
		Collection<T> modelCollection = null;
		if (reuseExistingCollection()) {
			modelCollection = ((IModel<Collection<T>>) getDefaultModel()).getObject();
			if (modelCollection != null) {
				modelCollection.clear();
			}
		}
		if (modelCollection == null) {
			Class<?> collectionClass = getObjectClass(getDefaultModel());
			if(List.class == collectionClass){
				modelCollection = new ArrayList<T>();
			} else if (Set.class == collectionClass){
				modelCollection = new HashSet<T>();
			} else {
				throw new IllegalArgumentException("Don't know how to initialize collection of type "+collectionClass+". Parth: "+getPath());
			}
		}
		int index = 0;
		for (IModel<? extends T> model : list) {
			T e = (T) model.getObject();
			if (e != null || !removeNullValues()) {
				setOrderProperty(e, index++);
				modelCollection.add(e);
			}
		}
		setDefaultModelObject(modelCollection);
	};

	/**
	 * 
	 * @return true means that the list in the model is reused if it already
	 *         exists. false means, a new list is created on updateModel
	 */
	protected boolean reuseExistingCollection() {
		return true;
	}

	/**
	 * Whether null element values shall be removed before the model is updated.
	 * Default is true.
	 * 
	 * @return
	 */
	protected boolean removeNullValues() {
		return true;
	}

	@Override
	protected void onDetach() {
		super.onDetach();
		// detach each list item
		for (IModel<? extends T> item : list) {
			item.detach();
		}
	}

	protected IModel<String> getItemLabelModel(final ListItem<IModel<? extends T>> item) {
		return new AbstractReadOnlyModel<String>() {

			/***/
			private static final long serialVersionUID = 1L;

			@Override
			public String getObject() {
				return getItemLabel(item);
			}

		};
	}

	protected String getItemLabel(final ListItem<IModel<? extends T>> item) {
		// TODO: It is very implicit to assume the parent provides the item
		// label...
		return getLocalizer().getString(findParent(DecoratorFragment.class).getId(), ListEditorPanel.this) + " " + (item.getIndex() + 1);
	}

	protected Component createItemComponent(String id, IModel<? extends T> model) {
		Component c = createItemComponent(id);
		if (c == null) {
			throw new IllegalArgumentException("Item Component is null. Forgotten to override createItemComponent?");
		}
		c.setDefaultModel(model);
		return c;
	}

	protected Component createItemComponent(String id) {
		return null;
	}

	/**
	 * Creates a model for a new element to be added to the list. The default
	 * implementation creates a LoadableDetachableModel that calls
	 * createNewElement
	 * 
	 * @return
	 */
	protected IModel<T> createNewModel(final Class<T> clazz) {

		return new LoadabaleTypeAwareModel<T>() {
			/***/
			private static final long serialVersionUID = 1L;

			@Override
			protected T load() {
				return null;
			}

			@SuppressWarnings("unchecked")
			@Override
			public Class<T> getObjectClass() {
				if (getObject() != null) {
					return (Class<T>) getObject().getClass();
				} else {
					return clazz;
				}
			}
		};
	}

	private static abstract class LoadabaleTypeAwareModel<T> extends LoadableDetachableModel<T> implements
			IObjectClassAwareModel<T> {

		/***/
		private static final long serialVersionUID = 1L;
	}

	/**
	 * The class of new elements to be created, when add button is clicked.
	 * Default is null, which means that the type is determined by examining the
	 * model object.
	 * 
	 * @return
	 */
	protected Class<T> getElementClass() {
		return null;
	};

	/**
	 * Given a collection typed property, this method creates an loadable and detachable IModel for each element in the Collection.
	 * 
	 * If the collection is a List, the index of the element is use to uniquely identify it.
	 * For entities, one might to wish to override this to
	 * use the IDs instead. (Which is more fail-safe when the one-to-many
	 * relationship is not really ordered or one might not depend on optimistic
	 * locking in the parent to detect concurrent modification of the list)
	 * 
	 * For Collections other than List, an index property must be provided.
	 * 
	 * TODO: Wie wird dann für die Konsistenz der Index-Property bei der Objekterzeugung gesorgt?
	 * 
	 * 
	 * @param collectionModel the model of the list / collection
	 * @param index the index of the element in the list / collection
	 * @return the Model for the element
	 */
	protected List<IModel<T>> createElementModelList(final IModel<Collection<T>> collectionModel) {
		Collection<T> collection = collectionModel.getObject();
		
		List<IModel<T>> result = new ArrayList<IModel<T>>();
		
		int i=0;
		for (T t : collection) {
			final int index = i;
			result.add( new LoadabaleTypeAwareModel<T>() {

				private static final long serialVersionUID = 1L;

				@Override
				protected T load() {
					// TODO: error handling if index does not exist anymore (due to
					// some concurrent modifications)
					
					Collection<T> collection = collectionModel.getObject();
					if(collection instanceof List<?>){
						return ((List<T>)collection).get(index);
					} else {
						
						//TODO: ineffecient ?
						for (T element : collection) {
							if (getOrderProperty(element) == index){
								return element;
							}		
						}	
						throw new IllegalStateException("No element found whose order property has value "+index);
						
					}
				}

				@SuppressWarnings("unchecked")
				@Override
				public Class<T> getObjectClass() {
					if (getObject() != null) {
						return (Class<T>) getObject().getClass();
					} else {
						return getElementClass();
					}
				}

			});
			i++;
		}
		
		
		return result;
	}

	/**
	 * The component to render when list items are removed or added. By default, this is the whole ListEditorPanel.
	 * @return the component to render
	 */
	protected Component getAjaxTargetComponent() {
		return ListEditorPanel.this;
	}

}
